浅析KPROBE_OVEERRIDE在错误注入中的使用
近期工作刚好涉及到相关内容,首先想到的是:使用eBPF技术去拿到目标函数执行流,再进行override替换。笔者在此之前并不清楚该想法是否可行。毕竟劫持函数流,并替换掉返回值这样的行为应该是和eBPF verifier
的初心是严重不匹配的。
但是抱着探索一番的态度,还是尝试性的打开了bpf_helpers.h
去查看相关API。
令我意想不到的是,该方法竟然真的存在(而且似乎已经存在很久了)。以上便是下文的Motivation,下面会详细介绍该机制,但是首先还是明晰一下错误注入这个概念。
错误注入技术是一种重要的方法,用于模拟和评估系统在异常或故障条件下的行为。通过故意引入错误,开发者和测试人员可以验证系统的健壮性、可靠性和容错能力。错误注入技术广泛应用于各种软件系统,包括操作系统、数据库、网络协议栈等,以确保这些系统在面对实际运行中可能遇到的错误时,能够正确响应并保持稳定运行。
在Linux内核开发和测试中,错误注入技术同样扮演着关键角色。内核开发者需要一种有效的方法来模拟和测试内核在各种异常情况下的表现。KPROBE_OVERRIDE作为一种内核错误注入机制,提供了一种强大的工具,允许开发者在运行时动态地修改内核代码的行为,从而模拟各种错误条件。
基于硬件的错误注入
基于用户态软件的错误注入
基于内核的错误注入
fault injection
特性。KPROBE_OVERIDE是基于内核CONFIG_KPROBE_OVERRIDE
的功能,使用bpf_helpers中的bpf_override_return()
函数进行实际的操作。
eBPF(Extended Berkeley Packet Filter)是一种强大的内核技术,最初设计用于网络数据包过滤,但现在已经扩展到多种用途,包括性能监控、安全监控、跟踪和错误注入等。eBPF 允许用户空间程序在内核中安全地执行受限的沙盒代码,而无需修改内核源码或加载内核模块。
关于eBPF是什么,以及eBPF的基本架构,不是本文的重点,可以参考:
eBPF基本概念-ByteTech
重要的一点是:
BPF Helpers 是内核提供的一组函数,eBPF 程序可以调用这些函数来执行各种操作,如访问系统信息、操作数据包、与用户空间通信等。这些函数提供了 eBPF 程序与内核交互的接口。
"Used for error injection, this helper uses kprobes to override the return value of the probed function, and to set it to rc. The first argument is the context regs on which the kprobe works."
kprobes
来覆盖被探测函数的返回值,并将其设置为 rc。"This helper works by setting the PC (program counter) to an override function which is run in place of the original probed function. This means the probed function is not run at all. The replacement function just returns with the required value."
"This helper has security implications, and thus is subject to restrictions. It is only available if the kernel was compiled with the CONFIG_BPF_KPROBE_OVERRIDE configuration option, and in this case it only works on functions tagged with ALLOW_ERROR_INJECTION in the kernel code."
"Also, the helper is only available for the architectures having the CONFIG_FUNCTION_ERROR_INJECTION option. As of this writing, x86 architecture is the only one to support this feature."
使用KPROBE_OVERRIDE特性,仅支持白名单内的函数,白名单通过ALLOW_ERROR_INJECTION宏进行标记。
// include/asm-generic/error_injection.h
#define ALLOW_ERROR_INJECTION(fname, _etype) \
static struct error_injection_entry __used \
__section("_error_injection_whitelist") \
_eil_addr_##fname = { \
.addr = (unsigned long)fname, \
.etype = EI_ETYPE_##_etype, \
};
_error_injection_whitelist
ELF段中,并用__used
属性确保了即使该实例在未被引用的情况下也不会被编译器优化掉。//include/asm-generic/error_injection.h
enum { //错误类型
EI_ETYPE_NONE, /* Dummy value for undefined case */
EI_ETYPE_NULL, /* Return NULL if failure */
EI_ETYPE_ERRNO, /* Return -ERRNO if failure */
EI_ETYPE_ERRNO_NULL, /* Return -ERRNO or NULL if failure */
EI_ETYPE_TRUE, /* Return true if failure */
};
-EIO
、-ENOMEM
等,返回一个int错误码。这三种可以满足大部分场景的使用了。
总结:当我们要针对一个函数进行标记的时候,需要清楚这个函数如果返回错误,这个错误应该是什么类型,根据需要传入、ERRNO/TRUE/NONE/NULL
等即可。
The Linux kernel configuration item CONFIG_BPF_KPROBE_OVERRIDE
:
CONFIG_BPF_EVENTS
and CONFIG_FUNCTION_ERROR_INJECTION
我们更关注的痛点,是不经常容易走到的错误处理分支,不同于其他高级语言的try...catch
机制,内核代码往往比较繁琐、自由,同时也会带来风险和疏忽。但是KPROBE_OVEERIED不同于传统错误注入。
例如:
硬件问题的难以模拟:
内核代码的繁琐和自由:
try...catch
)。错误处理分支的验证:
现有错误注入的范围和策略的不足
- 尽管内核提供了一些内置的错误注入机制,如 fail_make_request
和 fail_page_alloc
,但是通常只覆盖特定的内核子系统或功能模块。
- 现有内核错误注入策略不易修改和集成,需要去/sys/kernel/debug下调整触发概率,无法根据需要做更多操作,可编程性不强。
...
其实笔者刚开始是针对一些很常见的注入点进行尝试的,比如
kmalloc
,但是内核已经通过已有的CONFIG_FUNCTION_ERROR_INJECTION机制进行了一些集成,这里为了展示灵活性,我们选用更底层和不容易复现的错误注入进行尝试:
// drivers/net/ethernet/mellanox/mlx5/core/cmd.c#mlx5_cmd_exec
int mlx5_cmd_exec(struct mlx5_core_dev *dev, void *in, int in_size, void *out, int out_size)
//这里可能需要 #include <asm-generic/error-injection.h>
ALLOW_ERROR_INJECTION(mlx5_cmd_exec, ERRNO);
int mlx5_cmd_exec(struct mlx5_core_dev *dev, void *in, int in_size, void *out,
int out_size)
{
int err;
err = cmd_exec(dev, in, in_size, out, out_size, NULL, NULL, false);
return err ? : mlx5_cmd_check(dev, in, out);
}
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf/bpf_core_read.h"
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1024);
} fail_count SEC(".maps");
SEC("kprobe/mlx5_cmd_exec")
int Override_mlx5_cmd_exec(struct pt_regs *ctx) {
// 获取当前进程的tgid
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 获取当前进程名称
char comm[TASK_COMM_LEN];
bpf_get_current_comm(comm, TASK_COMM_LEN);
u64 timestamp = bpf_ktime_get_ns();
// 时间戳生成一个随机数
u64 random_value = timestamp % 100;
// 希望 10% 的概率返回 -EIO (-5)
if (random_value < 10) {
bpf_printk("[ERR_MLX_CMD_INJECTION] ----拦截cmd请求:%s : %d ----",comm, pid);
bpf_override_return(ctx, -5);
}
return 0;
}
char _license[] SEC("license") = "GPL";
接下来你可以选择:
直接通过clang的
clang -O2 -target bpf -c example.c -o example.o
+ bpftool的sudo bpftool prog load example.o /sys/fs/bpf/example
进行挂载使用一些其他的eBPF框架进行集成,如
libbpf-bootstrap/gobpf/cliuim
等。大部分框架的内核态eBPF文件都是使用C进行编写的(如上)
使用 ip link set ethX down
会偶尔触发(因为我们设置了10%的概率):
eBPF-VM
执行自定义代码,这会引入额外的开销,影响系统性能。Re:但是,每当我们针对新的需求,添加新的白名单函数,我们都要重新编译内核。
实际上我们完全可以通过一些手段,对符合模式(pattern)的所有函数进行一次性标记,内核诸多的宏其实给了我们很大的便利。我们完全可以针对业务需要去自定义这个模式
,去增强我们业务的稳定性,鲁棒性。
或者还有没有一种可能,每当有新的驱动.ko
模块的出现,我们的CI Pipline会把对应驱动的所有代码给到LLM(大模型)
,然后大模型会给出关键的函数以及对应ERROR,并自动生成eBPF程序,供我们使能KPROBE_OVERRIDE功能?
一查吓一跳,真有了!
不过是更加通用型的,完全可以被魔改后接入!
Just Think Different.